package com.polarrose.wsf.client.impl;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.SocketTimeoutException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.util.IdleConnectionTimeoutThread;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.polarrose.wsf.client.HttpProtocolException;
import com.polarrose.wsf.client.Invocation;
import com.polarrose.wsf.client.RequestInvoker;
import com.polarrose.wsf.client.ResultTransformationException;
import com.polarrose.wsf.client.WsfClientException;

public class HttpClientRequestInvoker implements RequestInvoker {

	public static final String DEFAULT_ENCODING = "UTF-8";

	private Logger logger = LoggerFactory.getLogger(HttpClientRequestInvoker.class);

	public static final String PARAM_VERSION = "Version";
	public static final String PARAM_ACTION = "Action";
	public static final String PARAM_SIGNATURE = "Signature";
	public static final String PARAM_SIGNATURE_VERSION = "SignatureVersion";
	public static final String PARAM_TIMESTAMP = "Timestamp";
	public static final String PARAM_ACCESS_KEY = "AccessKey";

	private HttpClient httpClient;
	private int connectionTimeout = 4000;
	private int defaultMaxConnectionsPerHost = 250;
	private int maxConnectionsPerHost = 250;
	private int maxTotalConnections = 2000;
	private int soTimeout = 15000;
	private int idleConnectionTimeout = 5000;
	private int idleConnectionTimeoutInterval = 5000;
	private boolean appendActionAndVersionAsQueryString;

	private String accessKey;
	private String secretKey;
	private String signatureVersion;

	public void setAccessKey(String accessKey) {
		this.accessKey = accessKey;
	}

	public void setSecretKey(String secretKey) {
		this.secretKey = secretKey;
	}

	public void setSignatureVersion(String signatureVersion) {
		this.signatureVersion = signatureVersion;
	}

	private synchronized void initializeDefaultHttpClient() {
		if (httpClient != null) {
			return;
		}
		MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
		connectionManager.getParams().setConnectionTimeout(connectionTimeout);
		connectionManager.getParams().setDefaultMaxConnectionsPerHost(defaultMaxConnectionsPerHost);
		connectionManager.getParams().setMaxConnectionsPerHost(HostConfiguration.ANY_HOST_CONFIGURATION, maxConnectionsPerHost);
		connectionManager.getParams().setMaxTotalConnections(maxTotalConnections);
		connectionManager.getParams().setSoTimeout(soTimeout);

		httpClient = new HttpClient(connectionManager);

		IdleConnectionTimeoutThread idleConnectionTimeoutThread = new IdleConnectionTimeoutThread();
		idleConnectionTimeoutThread.setName("IdleConnectionTimeoutThread");
		idleConnectionTimeoutThread.setConnectionTimeout(idleConnectionTimeout);
		idleConnectionTimeoutThread.setTimeoutInterval(idleConnectionTimeoutInterval);
		idleConnectionTimeoutThread.setDaemon(true);
		idleConnectionTimeoutThread.addConnectionManager(connectionManager);
		idleConnectionTimeoutThread.start();
	}

	public void invokeRequest(Invocation<?> invocation) throws WsfClientException {
		if (httpClient == null) {
			initializeDefaultHttpClient();
		}
		String endpoint = invocation.getEndpoint();
		String action = invocation.getAction();
		String version = invocation.getVersion();

		Map<String, String> parameters = invocation.getParameters();

		PostMethod method = null;
		try {

			parameters.put(PARAM_VERSION, version);
			parameters.put(PARAM_ACTION, action);

			if (accessKey != null) {
				parameters.put(PARAM_ACCESS_KEY, accessKey);
				parameters.put(PARAM_SIGNATURE_VERSION, signatureVersion);
				parameters.put(PARAM_TIMESTAMP, String.valueOf(System.currentTimeMillis() + 1000 * 30));
				parameters.put(PARAM_SIGNATURE, createSignature(secretKey, generateSignatureData(parameters)));
			}
			
			String queryString = "";
			if (appendActionAndVersionAsQueryString) {
				queryString = String.format("?action=%s&version=%s", action, version);
			}
			method = new PostMethod(endpoint + queryString);
			method.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");

			for (Entry<String, String> entry : parameters.entrySet()) {
				method.addParameter(entry.getKey(), entry.getValue());
			}

			String logEntry = createLogEntry(parameters);
			logger.debug("Requesting URL {}?{}", endpoint, logEntry);

			int status = httpClient.executeMethod(method);

			if (status == 200) {
				String response = IOUtils.toString(method.getResponseBodyAsStream(), DEFAULT_ENCODING);
				invocation.getResultTransformer().transform(response);
			} else {
				throw new HttpProtocolException(status, method.getStatusText());
			}
		} catch (SocketTimeoutException e) {
			throw new WsfClientException("Socket timeout occured.", e);
		} catch (IOException e) {
			logger.error("I/O error", e);
			throw new WsfClientException("An I/O error occured.", e);
		} catch (ResultTransformationException e) {
			throw new WsfClientException("Could not transform JSON response.", e);
		} finally {
			if (method != null) {
				method.releaseConnection();
			}
		}
	}

	private String createLogEntry(Map<String, String> parameters) throws UnsupportedEncodingException {
		StringBuilder buff = new StringBuilder();
		for (Entry<String, String> it : parameters.entrySet()) {
			String key = it.getKey();
			String value = it.getValue();

			buff.append(key);
			buff.append("=");
			if (value.length() < 1024) {
				buff.append(value);
			} else {
				buff.append(value.substring(0, 1024) + "...");
			}
			buff.append("&");
		}

		if (buff.length() > 0) {
			buff.setLength(buff.length() - 1);
		}
		return buff.toString();
	}

	public void setHttpClient(HttpClient httpClient) {
		this.httpClient = httpClient;
	}

	public void setConnectionTimeout(int connectionTimeout) {
		this.connectionTimeout = connectionTimeout;
	}

	public void setDefaultMaxConnectionsPerHost(int defaultMaxConnectionsPerHost) {
		this.defaultMaxConnectionsPerHost = defaultMaxConnectionsPerHost;
	}

	public void setIdleConnectionTimeout(int idleConnectionTimeout) {
		this.idleConnectionTimeout = idleConnectionTimeout;
	}

	public void setIdleConnectionTimeoutInterval(int idleConnectionTimeoutInterval) {
		this.idleConnectionTimeoutInterval = idleConnectionTimeoutInterval;
	}

	public void setMaxConnectionsPerHost(int maxConnectionsPerHost) {
		this.maxConnectionsPerHost = maxConnectionsPerHost;
	}

	public void setMaxTotalConnections(int maxTotalConnections) {
		this.maxTotalConnections = maxTotalConnections;
	}

	public void setSoTimeout(int soTimeout) {
		this.soTimeout = soTimeout;
	}

	public void setAppendActionAndVersionAsQueryString(boolean appendActionAndVersionAsQueryString) {
		this.appendActionAndVersionAsQueryString = appendActionAndVersionAsQueryString;
	}

	private String generateSignatureData(Map<String, String> parameters) {
		// Create a sorted list of the query parameter names

		List<String> sortedNames = new ArrayList<String>();
		for (String name : parameters.keySet()) {
			if (!name.equals("Signature")) {
				sortedNames.add(name.toString());
			}
		}

		Collections.sort(sortedNames, String.CASE_INSENSITIVE_ORDER);

		// Concatenate the sorted name/value pairs to form the signature data

		StringBuffer signatureData = new StringBuffer();
		for (String name : sortedNames) {
			signatureData.append(name);
			signatureData.append((String) parameters.get(name));
		}

		return signatureData.toString();
	}


	private final static String HMAC_SHA1 = "HmacSHA1";

	private String createSignature(String secretKey, String data) {
		try {
			// Create a Mac instance and initialize it with the AWS secret key
			SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(), HMAC_SHA1);
			Mac mac = Mac.getInstance(HMAC_SHA1);
			mac.init(secretKeySpec);
			// Compute the HMAC and return the base64 encoded result
			byte[] rawSignature = mac.doFinal(data.getBytes());
			return new String(Base64.encodeBase64(rawSignature));
		} catch (NoSuchAlgorithmException e) {
			throw new RuntimeException("Cannot get an instance of Mac " + HMAC_SHA1);
		} catch (InvalidKeyException e) {
			throw new RuntimeException("Invalid secret key: " + secretKey);
		}
	}

}
